<?php
/*
* Copyright 2007 Mark O'Sullivan
* This file is part of Vanilla.
* Vanilla is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
* Vanilla is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with Vanilla; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
* The latest source code for Vanilla is available at www.lussumo.com
* Contact Mark O'Sullivan at mark [at] lussumo [dot] com
*
* Description: Used to merge CSS and JavaScript files.
*/

InstallPacker();

class Pack {
	var $Body;
	var $Context;
	var $Extension;
	var $files_to_merge;		// Array of files (their path, full or partial) to merge
	var $files_to_merge_by_id;	// Array of references to files to merge
	var $LastModification;		// last modification of the pack
								// (higher last modification of the files to merge
	var $Name;
	var $Path;					// Directory path to the file
	var $root_path;				// Root path of the files_to_merge
								// $Configuration['APPLICATION_PATH'] by default
	var $Size;
	var $Type;					//  or css file
	
	
	
	function AddFile($path, $option = ''){
		if($this->Extension == 'js') {
			$this->files_to_merge[] = array(ForceString($path, ''), ForceBool($option, false));
		} else {
			$this->files_to_merge[] = array(ForceString($path, ''), ForceString($option, ''));
		}
	}
	
	function AddFileByID($key, $option = 0){
		$this->files_to_merge_by_id[]= array(ForceInt($key, 0), ForceInt($option, 0));
	}
	
	function Clear(){
		$this->Body = '';
		$this->Extension = '';
		$this->files_to_merge = array();
		$this->files_to_merge_by_id = array();
		$this->LastModification = 0;
		$this->Name = '';
		$this->Path = '';
		$this->root_path = $this->Context->Configuration['APPLICATION_PATH'];
		$this->Size = 0;
		$this->Type = 'text/plain';
	}
	
	function Pack(&$Context) {
		$this->Name = 'Pack';
		$this->Context = &$Context;
		$this->Clear();
	}
	
	function GetPropertyFromRequest(){
		//get the files to merge
		$files = ForceIncomingArray('f', array());
		foreach ($files as $file_merge) {
			$path = '';
			$option = '';
			$file = explode(',', $file_merge);
			$path = ForceInt($file[0], 0);
			$option = ForceInt($file[1], 0);
			$this->AddFileByID($path, $option);
		}

		$this->LastModification = ForceIncomingInt('tag', 0);
	}
}

class PackManager {
	
	var $Context;
	var $files_index_by_id;
	var $files_index_by_path;
	var $media_index_by_id;
	var $media_index_by_name;
	var $index_updated;

	
	//get the content of the pack form the cache
	function GetContentFromCache(&$pack){
		if ($pack->Name == "") return false;
		if ($pack->Path == "") return false;
		$file = $pack->Path . $pack->Name;
		$content = '';
		if(!$content = $this->_GetFiles($file)){
			return false;
		}
		$pack->Body = $content;
		return $pack->Body;
	}
	
	//get the content of the pack from files to pack
	function GetContentFromFiles(&$pack){
		//convert the file path from there id
		$this->GetFilesPath($pack);
		
		//Set the last modification for this package
		//will be the one of the most recent file
		$this->GetLastModification($pack);
		$pack->Body = '';
		$jsMin = new JSMin('', false);
		for ($i = 0; $i < count($pack->files_to_merge); $i++) {
			$content = '';
			$useMedia = 0;
			$file = $pack->root_path . $pack->files_to_merge[$i][0];
			//specify media dependencies for style sheets or not
			if ($pack->Extension == 'css'
			&& $pack->files_to_merge[$i][1] != ''
			&& $pack->files_to_merge[$i][1] != 'all'){
				$useMedia = 1;
				$content = '@media '. $pack->files_to_merge[$i][1] ." {\n";
			}
			
			//get the content
			if($content .= $this->_GetFiles($file)){
				// minify the script if asked
				if ($pack->Extension == 'js' && $pack->files_to_merge[$i][1]){
					$jsMin->JSMin_String($content);
					$content = $jsMin->minify();
				}
				
				//closing the @media bracket
				if ($useMedia) {
					$content .= "\n}";
				}
				$pack->Body .= $content."\n";
			} else {
				//should do something if it cant read the script
			}
		}
		$this->SavePack($pack);
		return $pack->Body;
	}

	function GetFilesPath(&$pack){
		foreach ($pack->files_to_merge_by_id as $file) {
			$path = '';
			$media = '';
			if (array_key_exists($file[0], $this->files_index_by_id)){
				$path = $this->files_index_by_id[$file[0]];
			}
			if (array_key_exists($file[1], $this->media_index_by_id)){
				$media = $this->media_index_by_id[$file[1]];
			}
			$pack->AddFile($path, $media);
		}
	}
	
	function GetFilesId(&$Pack){
		$isCss = ($Pack->Extension == 'css');
		foreach ($Pack->files_to_merge as $file) {
			$id = 0;
			$option = '';
			if (array_key_exists($file[0], $this->files_index_by_path)){
				$id = $this->files_index_by_path[$file[0]];
			} else {
				$id = $this->_AddFileToIndex($file[0]);
			}
			if($isCss) {
				if (array_key_exists($file[1], $this->media_index_by_name)){
					$option = $this->media_index_by_name[$file[1]];
				} else {
					$option = $this->_addMediaToIndex($file[1]);
				}
			} else {
				$option = $file[1];
			}
			$Pack->AddFileByID($id, $option);
		}
		if ($this->index_updated) {
			$this->_CacheFilesIndexes();
		}
	}
		
	//Get the path of the cache for the pack
	//(from the type and the list of file to pack)
	function GetPackPath(&$pack){
		if (count($pack->files_to_merge_by_id) > 0){
			$files = array();
			foreach ($pack->files_to_merge_by_id as $file) {
				$files[] = implode('',$file);
			}
			$name = md5(implode('', $files));
			$pack->Name = $name . '.' .$pack->LastModification . '.' .$pack->Extension;
		} else {
			$pack->Name = '';
			return false;
		}
		return $pack->Path . $pack->Name;
	}
	
	function GetPackUrl(&$Pack){
		if(count($Pack->files_to_merge)>0){
			$this->GetFilesId($Pack);
			$this->GetLastModification($Pack);
			if ($Pack->Extension == 'css'){
				$url = $this->Context->StyleUrl . 'packer.php?';
			} else {
				$url = $this->Context->Configuration['WEB_ROOT'] . 'js/packer.php?';
			}
			foreach ($Pack->files_to_merge_by_id as $file) {
				$url .= 'f[]='. implode(',', $file) . '&';
			}
			$url .= 'tag=' . $Pack->LastModification;
			return $url;
		} else {
			return false;
		}
	}
	
	function GetLastModification(&$pack){
		$files_mtime = array(); //collection of files last modification time
		foreach ($pack->files_to_merge as $file_to_merge) {
			$files_mtime[] = filemtime($pack->root_path . $file_to_merge[0]);
		}
		// get the higher higher modification time
		rsort($files_mtime, SORT_NUMERIC);
		// Set the last modification time of the pack
		if (count($files_mtime) > 0){
			$pack->LastModification = $files_mtime[0];
			return $pack->LastModification;
		} else {
			return false;
		}
	}
	
	function IsCached(&$Pack) {
		$user_cache_last_modification = 0;
		if (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) {
			$user_cache_last_modification = ForceInt($_SERVER['HTTP_IF_NONE_MATCH'], 0);
		} elseif (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
			if ($time = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
				$user_cache_last_modification = ForceInt($time, 0);
			}
		}
		if ($user_cache_last_modification
		&& $user_cache_last_modification == $Pack->LastModification) {
			return true;
		} else {
			return false;
		}
	}
	
	function IsPacked(&$Pack) {
		$path = $this->GetPackPath($Pack);	//The last modification is in the name 
		if( $path 
		&&	file_exists($path)) {
			return 1;
		} elseif (!$path) {
			return -1;
		} else {
			return 0;
		}
	}
	
	function PackManager(&$Context){
		$this->Name = 'PackManager';
		$this->Context = &$Context;
		$files_index_by_id = array();
		$files_index_by_path = array();
		$media_index_by_id = array();
		$media_index_by_name = array();
		@include($this->Context->Configuration['APPLICATION_PATH'].'conf/packer.php');
		$this->files_index_by_id = $files_index_by_id;
		$this->files_index_by_path = $files_index_by_path;
		$this->media_index_by_id = $media_index_by_id;
		$this->media_index_by_name = $media_index_by_name;
		$this->index_updated = 0;
	}

	function SavePack(&$pack){
		$this->_SaveFile($this->GetPackPath($pack), $pack->Body);
	}
	
	function ServePack(&$Pack){
		// FORCE_GZIP is only defined when Zlib Compression Functions is enable
		// When also allow to disable i, for example if you need to debug the packer
		if(defined('FORCE_GZIP')
		&& !$this->Context->Configuration['PACKER_DISABLE_GZIP']){ 
			ob_start('ob_gzhandler'); 	//create a gzip output buffering inside the main one,
										//when flushed I can get the length of the compressed output
			echo $Pack->Body;
			ob_end_flush(); // Flush the gzip output buffering
		} else {
			echo $Pack->Body;
		}
		$Pack->Size = ob_get_length();
	}

	function SetHeader(&$pack, $modified = 1){
		header('Expires: '.gmdate('D, d M Y H:i:s', time() + 31356000).' GMT'); // 1 year from now
		header('Last-Modified: '.gmdate('D, d M Y H:i:s', $pack->LastModification). ' GMT');
		header('ETag: '. $pack->LastModification);
		header('Cache-Control: private');
		if (!$modified){
			header($_SERVER["SERVER_PROTOCOL"] .' 304 Not Modified');	
		} else {
    	  	header('Content-Length: '.$pack->Size);
    	  	header('Content-Type: '. $pack->Type);
		}
	}
	
	//Get the file paths form the db
	function UpdateFilesIndexes(){
		$s = $this->Context->ObjectFactory->NewContextObject($this->Context, 'SqlBuilder');
		$s->SetMainTable('FilesToPack', 'f');
		$s->AddSelect(array('FileID', 'FilePath'), 'f');
		$FilesData = $this->Context->Database->Select($s, $this->Name, 'UpdateFilesIndexes', 'An error occurred while attempting to retrieve the list of js file.');
		while ($rows = $this->Context->Database->GetRow($FilesData)) {
			$this->files_index_by_id[$rows['FileID']] = $rows['FilePath'];
			$this->files_index_by_path[$rows['FilePath']] = $rows['FileID'];
		}
		$this->index_updated++;
	}
	
	function UpdateMediaIndexes(){
		$s = $this->Context->ObjectFactory->NewContextObject($this->Context, 'SqlBuilder');
		$s->SetMainTable('CssMedia', 'f');
		$s->AddSelect(array('MediaID', 'MediaName'), 'f');
		$MediaData = $this->Context->Database->Select($s, $this->Name, 'UpdateMediaIndexes', 'An error occurred while attempting to retrieve the list CSS Media.');
		while ($rows = $this->Context->Database->GetRow($MediaData)) {
			$this->media_index_by_id[$rows['MediaID']] = $rows['MediaName'];
			$this->media_index_by_name[$rows['MediaName']] = $rows['MediaID'];
		}
		$this->index_updated;
	}
	
	function _AddFileToIndex($file_path){
		$this->UpdateFilesIndexes();
		if (!array_key_exists($file_path, $this->files_index_by_path)){
			$s = $this->Context->ObjectFactory->NewContextObject($this->Context, 'SqlBuilder');
			$s->SetMainTable('FilesToPack', 'f');
			$s->AddFieldNameValue('FilePath', $file_path);
			if ($id = $this->Context->Database->Insert($s, $this->Name, '_AddFileToIndex', 'An error occurred while adding file to the packer index.')){
				$this->files_index_by_path[$file_path] = $id;
				$this->files_index_by_id[$id] = $file_path;
				return $id;
			} else return false;
		} else {
			return $this->files_index_by_path[$file_path];
		}
	}
	
	function _addMediaToIndex($media_name){
		$this->UpdateMediaIndexes();
		if (!array_key_exists($media_name, $this->media_index_by_name)){
			$s = $this->Context->ObjectFactory->NewContextObject($this->Context, 'SqlBuilder');
			$s->SetMainTable('CssMedia', 'f');
			$s->AddFieldNameValue('MediaName', $media_name);
			if($id = $this->Context->Database->Insert($s, $this->Name, '_addMediaToIndex', 'An error occurred while adding a media to the media index.')){
				$this->media_index_by_id[$id] = $media_name;
				$this->media_index_by_name[$media_name] = $id;
				return $id;
			} else {
				return false;
			}
		} else {
			return $this->media_index_by_name[$media_name];
		}
	}
	
	function _CacheFilesIndexes(){
		$content = "<?php\n"
		.'$files_index_by_id = unserialize(\'' . serialize($this->files_index_by_id) . "');\n"
		.'$files_index_by_path = unserialize(\'' . serialize($this->files_index_by_path) . "');\n"
		.'$media_index_by_id = unserialize(\'' . serialize($this->media_index_by_id) . "');\n"
		.'$media_index_by_name = unserialize(\'' . serialize($this->media_index_by_name) . "');\n"
		.'?>';
		$this->_SaveFile($this->Context->Configuration['APPLICATION_PATH'].'conf/packer.php', $content);
	}

	function _GetFiles($file_path){
		if (!$handle = fopen($file_path, "r")) {
			return false;
		}
		if (!$content = fread($handle, filesize($file_path))) {
			return false;
		}
		@fclose($handle);
		return $content;
	}
	
	function _SaveFile($file, $content){
		if (!$handle = @fopen($file, 'wb')) {
			return false;
		}
		if (!@fwrite($handle, $content)) {
			@fclose($handle);
			return false;
		}
		@fclose($handle);
		return true;
	}

}

function AddScriptaculous($component_names = array('builder', 'effects', 'dragdrop', 'controls', 'slider')) {
	global $Head;
	//Prototype can't be compressed
	$Head->AddScript('js/prototype.js', '~', '210', 1, 0);
	$position = 211;
	foreach ($component_names as $component_name) {
		$Head->AddScript('js/'.$component_name.'.js', '~', $position++, 1, 0);
	}
}

function InstallPacker(){
	global $Context;
	if (!isset($Context->Configuration['PACKER_VERSION'])){
		$senderObj = '';
		$senderMethod = 'PackerInstaller';
		$DropTable = "DROP TABLE IF EXISTS `". $Context->Configuration['DATABASE_TABLE_PREFIX'] . $Context->DatabaseTables['FilesToPack'] ."`";
		$Context->Database->Execute($DropTable, $senderObj, $senderMethod, 'Could not drop the FilesToPack Table');	
		$CreateTable = "CREATE TABLE IF NOT EXISTS `". $Context->Configuration['DATABASE_TABLE_PREFIX'] . $Context->DatabaseTables['FilesToPack'] ."` (\n"
  		."`". $Context->DatabaseColumns['FilesToPack']['FileID'] ."` int(11) NOT NULL auto_increment,\n"
  		."`". $Context->DatabaseColumns['FilesToPack']['FilePath'] ."` text NOT NULL,\n"
  		."PRIMARY KEY  (`". $Context->DatabaseColumns['FilesToPack']['FileID'] ."`)\n"
		.");";
		$Context->Database->Execute($CreateTable, $senderObj, $senderMethod, 'Could not create the FilesToPack Table');
		
		$DropTable = "DROP TABLE IF EXISTS `". $Context->Configuration['DATABASE_TABLE_PREFIX'] . $Context->DatabaseTables['CssMedia'] ."`";
		$Context->Database->Execute($DropTable, $senderObj, $senderMethod, 'Could not drop the CssMedia Table');	
		$CreateTable = "CREATE TABLE IF NOT EXISTS `". $Context->Configuration['DATABASE_TABLE_PREFIX'] . $Context->DatabaseTables['CssMedia'] ."` (\n"
  		."`". $Context->DatabaseColumns['CssMedia']['MediaID'] ."` int(11) NOT NULL auto_increment,\n"
  		."`". $Context->DatabaseColumns['CssMedia']['MediaName'] ."` varchar(255) NOT NULL,\n"
  		."PRIMARY KEY  (`". $Context->DatabaseColumns['CssMedia']['MediaID'] ."`),\n"
 		."KEY `". $Context->DatabaseColumns['CssMedia']['MediaName'] ."` (`". $Context->DatabaseColumns['CssMedia']['MediaName'] ."`)\n"
		.");";
		$Context->Database->Execute($CreateTable, $senderObj, $senderMethod, 'Could not create the CssMedia Table');
		AddConfigurationSetting($Context, 'PACKER_ON', '1');
		AddConfigurationSetting($Context, 'PACKER_DISABLE_GZIP', '0');
		if ($Context->ErrorManager->Iif()) {
			AddConfigurationSetting($Context, 'PACKER_VERSION', '0.3.beta');
		}
	}
	if (version_compare($Context->Configuration['PACKER_VERSION'], '0.4.beta', '<')) {
		AddConfigurationSetting($Context, 'PACKER_ON', '1');
		AddConfigurationSetting($Context, 'PACKER_DISABLE_GZIP', '0');
		AddConfigurationSetting($Context, 'PACKER_VERSION', '0.4.beta.1');
	}
}
?>